/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.modules.maven.jaxws.actions;

import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.PrimitiveTypeTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.VariableTree;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.util.ElementFilter;
import javax.swing.SwingUtilities;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.source.CancellableTask;
import org.netbeans.api.java.source.ClasspathInfo;
import org.netbeans.api.java.source.Comment;
import org.netbeans.api.java.source.Comment.Style;
import org.netbeans.api.java.source.ui.ScanDialog;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.TreeMaker;
import org.netbeans.modules.websvc.api.support.java.GenerationUtils;
import org.netbeans.modules.websvc.api.support.java.SourceUtils;
import org.netbeans.spi.java.classpath.support.ClassPathSupport;
import org.openide.cookies.SaveCookie;
import org.openide.loaders.DataObject;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import static org.netbeans.api.java.source.JavaSource.Phase;
import org.netbeans.api.java.source.WorkingCopy;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.progress.ProgressHandleFactory;
import org.netbeans.modules.j2ee.core.api.support.java.method.MethodCustomizer;
import org.netbeans.modules.j2ee.core.api.support.java.method.MethodCustomizerFactory;
//import org.netbeans.modules.j2ee.api.ejbjar.EjbJar;
import org.netbeans.modules.j2ee.core.api.support.java.method.MethodModel;
import org.netbeans.modules.j2ee.core.api.support.java.method.MethodModelSupport;
import org.openide.ErrorManager;
import org.openide.filesystems.FileObject;

/**
 * Helper for adding WS Operation to Web Service.
 * @author Milan Kuchtiak
 */
public class AddWsOperationHelper {
    private static final ClassPath EMPTY_PATH = ClassPathSupport.createClassPath(new URL[0]);
    
    private final String name;
    private final boolean createAnnotations;
    private MethodModel method;
    
    public AddWsOperationHelper(String name, boolean flag) {
        this.name = name;
        this.createAnnotations = flag;
    }
    
    public AddWsOperationHelper(String name) {
        this(name,true);
    }
    
    protected MethodModel getPrototypeMethod() {
        return MethodModel.create(
                NbBundle.getMessage(AddWsOperationHelper.class,"TXT_DefaultOperationName"), //NOI18N
                "java.lang.String", //NOI18N
                "",
                Collections.<MethodModel.Variable>emptyList(),
                Collections.<String>emptyList(),
                Collections.<Modifier>emptySet()
                );
    }
    
    public String getTitle() {
        return name;
    }
    
    protected MethodCustomizer createDialog(FileObject fileObject, MethodModel methodModel) throws IOException {
        
        return MethodCustomizerFactory.operationMethod(
                getTitle(),
                methodModel,
                ClasspathInfo.create(
                    ClassPath.getClassPath(fileObject, ClassPath.BOOT), // JDK classes
                    ClassPath.getClassPath(fileObject, ClassPath.COMPILE), // classpath from dependent projects and libraries
                    ClassPath.getClassPath(fileObject, ClassPath.SOURCE)), // source classpath
                getExistingMethods(fileObject));
    }
    
    public void addMethod(FileObject fileObject, String className) throws IOException {
        if (className == null) {
            return;
        }
        method = getPrototypeMethod();
        MethodCustomizer methodCustomizer = createDialog(fileObject, method);
        if (methodCustomizer.customizeMethod()) {
            try {
                
                method = methodCustomizer.getMethodModel();
                okButtonPressed(method, fileObject, className);
            } catch (IOException ioe) {
                ErrorManager.getDefault().notify(ioe);
            }
        }
        else{  //user pressed cancel button
            method = null;
        }
    }
    
    /**
     *  Variant of addMethod(FileObject, String)which returns the final MethodModel.
     */ 
    public MethodModel getMethodModel(FileObject fileObject, String className) throws IOException{
        addMethod(fileObject, className);
        return method;
    }
    
    protected void okButtonPressed(MethodModel method, FileObject implClassFo, String className) throws IOException {
        addOperation(method, implClassFo);
    }
    
//    protected FileObject getDDFile(FileObject fileObject) {
//        return EjbJar.getEjbJar(fileObject).getDeploymentDescriptor();
//    }
    
    /*
     * Adds a method definition to the the implementation class
     */
    private void addOperation(final MethodModel methodModel, final FileObject implClassFo) {
        final JavaSource targetSource = JavaSource.forFileObject(implClassFo);
        final ProgressHandle handle = ProgressHandleFactory.createHandle(NbBundle.getMessage(AddWsOperationHelper.class, "MSG_AddingNewOperation", methodModel.getName()));
        handle.start(100);
        final String[] seiClass = new String[1];
        final CancellableTask<WorkingCopy> modificationTask = new CancellableTask<WorkingCopy>() {
            @Override
            public void run(WorkingCopy workingCopy) throws IOException {
                workingCopy.toPhase(Phase.RESOLVED);
                MethodTree method = MethodModelSupport.createMethodTree(workingCopy, methodModel);
                if (method!=null) {
                    TreeMaker make = workingCopy.getTreeMaker();
                    TypeElement typeElement = SourceUtils.getPublicTopLevelElement(workingCopy);
                    if (typeElement!=null) {

                        boolean increaseProgress = true;
                        
                        if (createAnnotations) {
                            if (seiClass[0] == null) {
                                seiClass[0] = getEndpointInterface(typeElement, workingCopy);
                            } else {
                                seiClass[0] = null;
                                increaseProgress = false;
                            }                           
                        }
                        
                        if (increaseProgress) handle.progress(20);
                        
                        ClassTree javaClass = workingCopy.getTrees().getTree(typeElement);
                        TypeElement webMethodAn = workingCopy.getElements().getTypeElement("javax.jws.WebMethod"); //NOI18N
                        TypeElement webParamAn = workingCopy.getElements().getTypeElement("javax.jws.WebParam"); //NOI18N
                                               
                       // Public modifier
                        ModifiersTree modifiersTree = make.Modifiers(
                                Collections.<Modifier>singleton(Modifier.PUBLIC),
                                Collections.<AnnotationTree>emptyList()
                                );                        
                        
                        // add @WebMethod annotation
                        if(createAnnotations && seiClass[0] == null) {

                            String methodName = method.getName().toString();
                            // find value for @WebMethod:oparationName
                            String operationName = findNewOperationName(typeElement, workingCopy, methodName);
                                                     
                            AssignmentTree opName = make.Assignment(make.Identifier("operationName"), make.Literal(operationName)); //NOI18N

                            AnnotationTree webMethodAnnotation = make.Annotation(
                                    make.QualIdent(webMethodAn),
                                    Collections.<ExpressionTree>singletonList(opName)
                                    );
                            modifiersTree = make.addModifiersAnnotation(modifiersTree, webMethodAnnotation);

                            // add @Oneway annotation
                            
                            boolean isOneWay = false;
                            if (Kind.PRIMITIVE_TYPE == method.getReturnType().getKind()) {
                                PrimitiveTypeTree primitiveType = (PrimitiveTypeTree)method.getReturnType();
                                if (TypeKind.VOID == primitiveType.getPrimitiveTypeKind()) {
                                    if (method.getThrows().size() == 0) {
                                        isOneWay = true;
                                        TypeElement oneWayAn = workingCopy.getElements().getTypeElement("javax.jws.Oneway"); //NOI18N
                                        AnnotationTree oneWayAnnotation = make.Annotation(
                                                make.QualIdent(oneWayAn),
                                                Collections.<ExpressionTree>emptyList()
                                                );

                                            modifiersTree = make.addModifiersAnnotation(modifiersTree, oneWayAnnotation);
                                    }
                                }
                            }
                            if (!methodName.equals(operationName)) {
                                // generate Request/Response wrapper annotations to avoid class conflicts
                                // this enables to generate operations with identical method names
                                String packagePrefix = getPackagePrefix(typeElement.getQualifiedName().toString());

                                TypeElement reqWrapperAn = workingCopy.getElements().getTypeElement("javax.xml.ws.RequestWrapper"); //NOI18N
                                AssignmentTree className = make.Assignment(make.Identifier("className"), make.Literal(packagePrefix+operationName)); //NOI18N
                                AnnotationTree reqWrapperAnnotation = make.Annotation(
                                        make.QualIdent(reqWrapperAn),
                                        Collections.<ExpressionTree>singletonList(className)
                                        );
                                modifiersTree = make.addModifiersAnnotation(modifiersTree, reqWrapperAnnotation);
                                if (!isOneWay) {
                                    TypeElement resWrapperAn = workingCopy.getElements().getTypeElement("javax.xml.ws.ResponseWrapper"); //NOI18N
                                    className = make.Assignment(make.Identifier("className"), make.Literal(packagePrefix+operationName+"Response")); //NOI18N
                                    AnnotationTree resWrapperAnnotation = make.Annotation(
                                            make.QualIdent(resWrapperAn),
                                            Collections.<ExpressionTree>singletonList(className)
                                            );
                                    modifiersTree = make.addModifiersAnnotation(modifiersTree, resWrapperAnnotation);
                                }
                            }
                        }
                        
                        if (increaseProgress) handle.progress(40);
                        
                        // add @WebParam annotations
                        List<? extends VariableTree> parameters = method.getParameters();
                        List<VariableTree> newParameters = new ArrayList<VariableTree>();
                        
                        if(createAnnotations && seiClass[0] == null) {
                            for (VariableTree param:parameters) {
                                AnnotationTree paramAnnotation = make.Annotation(
                                        make.QualIdent(webParamAn),
                                        Collections.<ExpressionTree>singletonList(
                                        make.Assignment(make.Identifier("name"), make.Literal(param.getName().toString()))) //NOI18N
                                        );
                                GenerationUtils genUtils = GenerationUtils.newInstance(workingCopy);
                                newParameters.add(genUtils.addAnnotation(param, paramAnnotation));
                            }
                        } else {
                            newParameters.addAll(parameters);
                        }
                                               
                        if (increaseProgress) handle.progress(70);
                        // create new (annotated) method
                        MethodTree  annotatedMethod = typeElement.getKind() == ElementKind.CLASS ?
                            make.Method(
                                modifiersTree,
                                method.getName(),
                                method.getReturnType(),
                                method.getTypeParameters(),
                                newParameters,
                                method.getThrows(),
                                getMethodBody(method.getReturnType()), //NOI18N
                                (ExpressionTree)method.getDefaultValue()) :
                            make.Method(
                                modifiersTree,
                                method.getName(),
                                method.getReturnType(),
                                method.getTypeParameters(),
                                newParameters,
                                method.getThrows(),
                                (BlockTree)null,
                                (ExpressionTree)method.getDefaultValue());
                        Comment comment = Comment.create(Style.JAVADOC, -2, -2, -2,
                                NbBundle.getMessage(AddWsOperationHelper.class, "TXT_WSOperation"));
                        make.addComment(annotatedMethod, comment, true);
                        
                        if (increaseProgress) handle.progress(90);
                        ClassTree modifiedClass = make.addClassMember(javaClass,annotatedMethod);
                        workingCopy.rewrite(javaClass, modifiedClass);
                    }
                }
            }
            @Override
            public void cancel() {
            }
        };
        final Runnable runnable = new Runnable() {
            
            @Override
            public void run() {
                doAddOperation(implClassFo, targetSource, handle, seiClass,
                        modificationTask);                
            }
        };
        final String title = NbBundle.getMessage(AddWsOperationHelper.class, 
                "LBL_AddOperation");        // NOI18N
        if (SwingUtilities.isEventDispatchThread()) {
            ScanDialog.runWhenScanFinished( runnable, title );
        } else {
            SwingUtilities.invokeLater( new Runnable() {
                
                @Override
                public void run() {
                    ScanDialog.runWhenScanFinished( runnable, title );                    
                }
            });       
        }

    }

    private void doAddOperation( final FileObject implClassFo,
            final JavaSource targetSource, final ProgressHandle handle,
            final String[] seiClass,
            final CancellableTask<WorkingCopy> modificationTask )
    {
        RequestProcessor.getDefault().post(new Runnable() {
            @Override
            public void run() {
                try {
                    targetSource.runModificationTask(modificationTask).commit();
                    // add method to SEI class
                    if (seiClass[0] != null) {
                        ClassPath sourceCP = ClassPath.getClassPath(implClassFo, ClassPath.SOURCE);
                        FileObject seiFo = sourceCP.findResource(seiClass[0].replace('.', '/')+".java"); //NOI18N
                        if (seiFo != null) {
                            JavaSource seiSource = JavaSource.forFileObject(seiFo);
                            seiSource.runModificationTask(modificationTask).commit();
                            saveFile(seiFo);
                        }
                    }
                    saveFile(implClassFo);
                } catch (IOException ex) {
                    ErrorManager.getDefault().notify(ex);
                } finally {
                    handle.finish();
                }                
            }
        });
    }

    private String getEndpointInterface(TypeElement classEl, CompilationController controller) {
        TypeElement wsElement = controller.getElements().getTypeElement("javax.jws.WebService"); //NOI18N
        if (wsElement != null) {
            List<? extends AnnotationMirror> annotations = classEl.getAnnotationMirrors();
            for (AnnotationMirror anMirror : annotations) {
                if (controller.getTypes().isSameType(wsElement.asType(), anMirror.getAnnotationType())) {
                    Map<? extends ExecutableElement, ? extends AnnotationValue> expressions = anMirror.getElementValues();
                    for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry: expressions.entrySet()) {
                        if (entry.getKey().getSimpleName().contentEquals("endpointInterface")) { //NOI18N
                            String value = (String) expressions.get(entry.getKey()).getValue();
                            if (value != null) {
                                TypeElement seiEl = controller.getElements().getTypeElement(value);
                                if (seiEl != null) {
                                    return seiEl.getQualifiedName().toString();
                                }
                            }
                        }
                    }
                } // end if
            }
        }
        return null;
    }
    
    private String findNewOperationName(TypeElement classEl, CompilationController controller, String suggestedMethodName) 
        throws IOException {
        
        TypeElement methodElement = controller.getElements().getTypeElement("javax.jws.WebMethod"); //NOI18N
        Set<String> operationNames = new HashSet<String>();
        if (methodElement != null) {
            List<ExecutableElement> methods = getMethods(controller,classEl);
            for (ExecutableElement m:methods) {
                String opName = null;
                List<? extends AnnotationMirror> annotations = m.getAnnotationMirrors();
                for (AnnotationMirror anMirror : annotations) {
                    if (controller.getTypes().isSameType(methodElement.asType(), anMirror.getAnnotationType())) {
                        Map<? extends ExecutableElement, ? extends AnnotationValue> expressions = anMirror.getElementValues();
                        for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry: expressions.entrySet()) {
                            if (entry.getKey().getSimpleName().contentEquals("operationName")) { //NOI18N
                                opName = (String) expressions.get(entry.getKey()).getValue();
                                break;
                            }
                        }
                    } // end if
                    if (opName != null) break;
                } //enfd for
                if (opName == null) opName = m.getSimpleName().toString();
                operationNames.add(opName);
            }
        }
        return findNewOperationName(operationNames, suggestedMethodName);
    }
    
    
    private String findNewOperationName(Set<String> operationNames, String suggestedMethodName) {       
        int i=0;
        String newName = suggestedMethodName; //NOI18N
        while(operationNames.contains(newName)) {
            newName = suggestedMethodName+"_"+String.valueOf(++i); //NOI18N
        }
        return newName;
    }
    
    
    private String getPackagePrefix (String className) {
        int lastDot = className.indexOf("."); //NOI18N
        if (lastDot > 0) return className.substring(0,lastDot+1);
        else return "";
    }
    
    private void saveFile(FileObject file) throws IOException {
        DataObject dataObject = DataObject.find(file);
        if (dataObject!=null) {
            SaveCookie cookie = dataObject.getCookie(SaveCookie.class);
            if (cookie!=null) cookie.save();
        }        
    }
    
    private String getMethodBody(Tree returnType) {
        String body = null;
        if (Kind.PRIMITIVE_TYPE == returnType.getKind()) {
            TypeKind type = ((PrimitiveTypeTree)returnType).getPrimitiveTypeKind();
            if (TypeKind.VOID == type) body = ""; //NOI18N
            else if (TypeKind.BOOLEAN == type) body = "return false;"; // NOI18N
            else if (TypeKind.INT == type) body = "return 0;"; // NOI18N
            else if (TypeKind.LONG == type) body = "return 0;"; // NOI18N
            else if (TypeKind.FLOAT == type) body = "return 0.0;"; // NOI18N
            else if (TypeKind.DOUBLE == type) body = "return 0.0;"; // NOI18N
            else if (TypeKind.BYTE == type) body = "return 0;"; // NOI18N
            else if (TypeKind.SHORT == type) body = "return 0;"; // NOI18N
            else if (TypeKind.CHAR == type) body = "return ' ';"; // NOI18N
            else body = "return null"; //NOI18N
        } else
            body = "return null"; //NOI18N
        return "{\n\t\t"+NbBundle.getMessage(AddWsOperationHelper.class, "TXT_TodoComment")+"\n"+body+"\n}";
    }
    /*
    protected static MethodsNode getMethodsNode() {
        Node[] nodes = Utilities.actionsGlobalContext().lookup(new Lookup.Template<Node>(Node.class)).allInstances().toArray(new Node[0]);
        if (nodes.length != 1) {
            return null;
        }
        return nodes[0].getLookup().lookup(MethodsNode.class);
    }
     */
    
    private Collection<MethodModel> getExistingMethods(FileObject implClass) {
        final JavaSource javaSource = JavaSource.forFileObject(implClass);
        final ResultHolder<MethodModel> result = new ResultHolder<MethodModel>();
        if (javaSource!=null) {
            final CancellableTask<CompilationController> task = 
                new CancellableTask<CompilationController>() 
                {
                
                @Override
                public void run(CompilationController controller) throws IOException {
                    controller.toPhase(Phase.ELEMENTS_RESOLVED);
                    TypeElement typeElement = SourceUtils.
                        getPublicTopLevelElement(controller);
                    Collection<MethodModel> wsOperations = new ArrayList<MethodModel>();
                    if (typeElement!=null) {
                        // find methods
                        List<ExecutableElement> allMethods = getMethods(controller, 
                                typeElement);
                        boolean foundWebMethodAnnotation=false;
                        for(ExecutableElement method:allMethods) {
                            // check if return type is a valid type
                            if (method.getReturnType().getKind() == TypeKind.ERROR) break;
                            // check if param types are valid types
                            
                            boolean validParamTypes = true;
                            List<? extends VariableElement> params = method.getParameters();
                            for (VariableElement param:params) {
                                if (param.asType().getKind() == TypeKind.ERROR) {
                                    validParamTypes = false;
                                    break;
                                }
                            }
                            if (validParamTypes) {
                                MethodModel methodModel = MethodModelSupport.
                                    createMethodModel(controller, method);
                                wsOperations.add(methodModel);
                            }
                        } // for
                    }
                    result.setResult(wsOperations);
                }
                @Override
                public void cancel() {}
            };
            final Runnable runnable = new Runnable() {
                
                @Override
                public void run() {
                    try {
                        javaSource.runUserActionTask(task, true);
                    } catch (IOException ex) {
                        ErrorManager.getDefault().notify(ex);
                    }
                }
            };
            final String title = NbBundle.getMessage(AddWsOperationHelper.class,
                    "LBL_FindMethods") ;                // NOI18N
            if ( SwingUtilities.isEventDispatchThread() ){
                ScanDialog.runWhenScanFinished(runnable,title  );
            }
            else {
                try {
                    SwingUtilities.invokeAndWait( new Runnable() {
                    
                        @Override
                        public void run() {
                            ScanDialog.runWhenScanFinished(runnable,title  );                        
                        }
                    });
                }
                catch (InvocationTargetException e ){
                    ErrorManager.getDefault().notify(e);
                }
                catch( InterruptedException e ){
                    ErrorManager.getDefault().notify(e);
                }
            }
        }
        return result.getResult();
    }
    
    private List<ExecutableElement> getMethods(CompilationController controller, TypeElement classElement) throws IOException {
        List<? extends Element> members = classElement.getEnclosedElements();
        List<ExecutableElement> methods = ElementFilter.methodsIn(members);
        List<ExecutableElement> publicMethods = new ArrayList<ExecutableElement>();
        for (ExecutableElement m:methods) {
            //Set<Modifier> modifiers = method.getModifiers();
            //if (modifiers.contains(Modifier.PUBLIC)) {
            publicMethods.add(m);
            //}
        }
        return publicMethods;
    }
    
    /** Holder class for result
     */
    private static class ResultHolder<E> {
        private Collection<E> result;
        
        public Collection<E> getResult() {
            return result;
        }
        
        public void setResult(Collection<E> result) {
            this.result=result;
        }
    }
}
